using System;
using System.Globalization;

namespace MvvmFx.Common.Helpers
{
    public enum Quarter
    {
        First = 1,
        Second = 2,
        Third = 3,
        Fourth = 4
    }

    public enum Month
    {
        January = 1,
        February = 2,
        March = 3,
        April = 4,
        May = 5,
        June = 6,
        July = 7,
        August = 8,
        September = 9,
        October = 10,
        November = 11,
        December = 12
    }

    public enum Period
    {
        Daily,
        Monthly,
        Quarterly,
        HalfYearly,
        Yearly
    }

    /// <summary>
    /// Static utility functions for manipulating and using Dates.
    /// </summary>
    public static class DateHelper
    {
        #region Quarters

        public static DateTime GetStartOfQuarter(int year, Quarter qtr)
        {
            switch (qtr)
            {
                case Quarter.First: // 1st Quarter = January 1 to March 31
                    return new DateTime(year, 1, 1, 0, 0, 0, 0);
                case Quarter.Second: // 2nd Quarter = April 1 to June 30
                    return new DateTime(year, 4, 1, 0, 0, 0, 0);
                case Quarter.Third: // 3rd Quarter = July 1 to September 30
                    return new DateTime(year, 7, 1, 0, 0, 0, 0);
                case Quarter.Fourth: // 4th Quarter = October 1 to December 31
                    return new DateTime(year, 10, 1, 0, 0, 0, 0);
                default:
                    throw new ArgumentOutOfRangeException("qtr");
            }
        }

        public static DateTime GetEndOfQuarter(int year, Quarter qtr)
        {
            // 1st Quarter = January 1 to March 31
            if (qtr == Quarter.First)
                return new DateTime(year, 3,
                                    DateTime.DaysInMonth(year, 3), 23, 59, 59, 999);

            else if (qtr == Quarter.Second) // 2nd Quarter = April 1 to June 30
                return new DateTime(year, 6,
                                    DateTime.DaysInMonth(year, 6), 23, 59, 59, 999);

            else if (qtr == Quarter.Third) // 3rd Quarter = July 1 to September 30
                return new DateTime(year, 9,
                                    DateTime.DaysInMonth(year, 9), 23, 59, 59, 999);

            else // 4th Quarter = October 1 to December 31
                return new DateTime(year, 12,
                                    DateTime.DaysInMonth(year, 12), 23, 59, 59, 999);
        }

        public static Quarter GetQuarter(Month month)
        {
            //Fastest solution
            switch (month)
            {
                case Month.January:
                case Month.February:
                case Month.March:
                    return Quarter.First;
                case Month.April:
                case Month.May:
                case Month.June:
                    return Quarter.Second;
                case Month.July:
                case Month.August:
                case Month.September:
                    return Quarter.Third;
                case Month.October:
                case Month.November:
                case Month.December:
                    return Quarter.Fourth;
                default:
                    throw new ArgumentOutOfRangeException("month");
            }

            //Original solution
            //if (month <= Month.March)
            //    // 1st Quarter = January 1 to March 31
            //    return Quarter.First;
            //else if ((month >= Month.April) && (month <= Month.June))
            //    // 2nd Quarter = April 1 to June 30
            //    return Quarter.Second;
            //else if ((month >= Month.July) && (month <= Month.September))
            //    // 3rd Quarter = July 1 to September 30
            //    return Quarter.Third;
            //else // 4th Quarter = October 1 to December 31
            //    return Quarter.Fourth;

            //Elegent solution
            //Int32 temp;
            //return Enum.Parse(typeof(Quarter), Math.DivRem(month + 2, 3, out temp));
        }

        public static DateTime GetEndOfLastQuarter()
        {
            if ((Month)DateTime.Now.Month <= Month.March)
                //go to last quarter of previous year
                return GetEndOfQuarter(DateTime.Now.Year - 1, Quarter.Fourth);
            else //return last quarter of current year
                return GetEndOfQuarter(DateTime.Now.Year,
                                       GetQuarter((Month)DateTime.Now.Month));
        }

        public static DateTime GetStartOfLastQuarter()
        {
            if ((Month)DateTime.Now.Month <= Month.March)
                //go to last quarter of previous year
                return GetStartOfQuarter(DateTime.Now.Year - 1, Quarter.Fourth);
            else //return last quarter of current year
                return GetStartOfQuarter(DateTime.Now.Year,
                                         GetQuarter((Month)DateTime.Now.Month));
        }

        public static DateTime GetStartOfCurrentQuarter()
        {
            return GetStartOfQuarter(DateTime.Now.Year,
                                     GetQuarter((Month)DateTime.Now.Month));
        }

        public static DateTime GetEndOfCurrentQuarter()
        {
            return GetEndOfQuarter(DateTime.Now.Year,
                                   GetQuarter((Month)DateTime.Now.Month));
        }

        #endregion

        #region Weeks

        public static DateTime GetStartOfLastWeek()
        {
            int DaysToSubtract = (int)DateTime.Now.DayOfWeek + 7;
            DateTime dt =
                DateTime.Now.Subtract(System.TimeSpan.FromDays(DaysToSubtract));
            return new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0, 0);
        }

        public static DateTime GetEndOfLastWeek()
        {
            DateTime dt = GetStartOfLastWeek().AddDays(6);
            return new DateTime(dt.Year, dt.Month, dt.Day, 23, 59, 59, 999);
        }

        public static DateTime GetStartOfCurrentWeek()
        {
            int DaysToSubtract = (int)DateTime.Now.DayOfWeek;
            DateTime dt =
                DateTime.Now.Subtract(System.TimeSpan.FromDays(DaysToSubtract));
            return new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0, 0);
        }

        public static DateTime GetEndOfCurrentWeek()
        {
            DateTime dt = GetStartOfCurrentWeek().AddDays(6);
            return new DateTime(dt.Year, dt.Month, dt.Day, 23, 59, 59, 999);
        }

        #endregion

        #region Months

        public static int GetMonths(DateTime date1, DateTime date2)
        {
            if (date1.Date != date2.Date)
                if (IsEndOfMonth(date2))
                    date2 = date2.AddDays(1);

            return (date2.Year * 12 + date2.Month) - (date1.Year * 12 + date1.Month);
        }

        /// <summary>
        /// Used to get number of months modification for impact changes to entry or exit criteria
        /// </summary>
        /// <param name="date1"></param>
        /// <param name="date2"></param>
        /// <returns></returns>
        public static int GetExactMonths(DateTime date1, DateTime date2)
        {
            if (date1.Date != date2.Date)
            {
                if (IsEndOfMonth(date1))
                    date1 = date1.AddDays(1);

                if (IsEndOfMonth(date2))
                    date2 = date2.AddDays(1);
            }

            return (date2.Year * 12 + date2.Month) - (date1.Year * 12 + date1.Month);
        }

        public static DateTime GetStartOfMonth(Month month, int year)
        {
            return GetStartOfMonth((int)month, year);
        }

        public static DateTime GetStartOfMonth(int month, int year)
        {
            return new DateTime(year, month, 1, 0, 0, 0, 0);
        }

        public static Boolean IsStartOfMonth(DateTime date)
        {
            return date.Day == 1;
        }        

        public static Boolean IsEndOfMonth(DateTime date)
        {
            return date.Day == DateTime.DaysInMonth(date.Year, date.Month);
        }

        public static DateTime GetEndOfMonth(Month month, int year)
        {
            return GetEndOfMonth((int)month, year);
        }

        public static DateTime GetEndOfMonth(int month, int year)
        {
            return new DateTime(year, month, DateTime.DaysInMonth(year, month), 23, 59, 59, 999);
        }

        public static DateTime GetStartOfLastMonth()
        {
            if (DateTime.Now.Month == 1)
                return GetStartOfMonth(12, DateTime.Now.Year - 1);
            else
                return GetStartOfMonth(DateTime.Now.Month - 1, DateTime.Now.Year);
        }

        public static DateTime GetEndOfLastMonth()
        {
            if (DateTime.Now.Month == 1)
                return GetEndOfMonth(12, DateTime.Now.Year - 1);
            else
                return GetEndOfMonth(DateTime.Now.Month - 1, DateTime.Now.Year);
        }

        public static DateTime GetStartOfCurrentMonth()
        {
            return GetStartOfMonth(DateTime.Now.Month, DateTime.Now.Year);
        }

        public static DateTime GetEndOfCurrentMonth()
        {
            return GetEndOfMonth(DateTime.Now.Month, DateTime.Now.Year);
        }

        #endregion

        #region HalfYears

        public static int GetYearHalf(int month)
        {
            switch (month)
            {
                case 1:
                case 2:
                case 3:
                case 4:
                case 5:
                case 6:
                    return 1;
                case 7:
                case 8:
                case 9:
                case 10:
                case 11:
                case 12:
                default:
                    return 2;
            }
        }

        public static DateTime GetStartOfHalfYear(int year, int month)
        {
            if (month <= 6)
            {
                return new DateTime(year, 1, 1, 0, 0, 0, 0);
            }
            else
            {
                return new DateTime(year, 7, 1, 0, 0, 0, 0);
            }
        }

        public static DateTime GetEndOfHalfYear(int year, int month)
        {
            if (month <= 6)
            {
                return new DateTime(year, 6, 30, 0, 0, 0, 0);
            }
            else
            {
                return new DateTime(year, 12, 31, 0, 0, 0, 0);
            }
        }

        #endregion

        #region Years

        public static DateTime GetStartOfYear(int year)
        {
            return new DateTime(year, 1, 1, 0, 0, 0, 0);
        }

        public static DateTime GetEndOfYear(int year)
        {
            return new DateTime(year, 12, DateTime.DaysInMonth(year, 12), 23, 59, 59, 999);
        }

        public static DateTime GetStartOfLastYear()
        {
            return GetStartOfYear(DateTime.Now.Year - 1);
        }

        public static DateTime GetEndOfLastYear()
        {
            return GetEndOfYear(DateTime.Now.Year - 1);
        }

        public static DateTime GetStartOfCurrentYear()
        {
            return GetStartOfYear(DateTime.Now.Year);
        }

        public static DateTime GetEndOfCurrentYear()
        {
            return GetEndOfYear(DateTime.Now.Year);
        }

        /// <summary>
        /// Examines the month and day of a <see cref="DateTime"/> object to
        /// see if it represents the first day of a year.
        /// </summary>
        /// <param name="date">The date to test.</param>
        /// <returns>If the date's month is 1 (January) and the day is 1, the 
        /// method return true, otherwise false.</returns>
        public static bool IsStartOfYear(DateTime date)
        {
            return date.Month == 1 && date.Day == 1;
        }

        #endregion

        #region Days

        public static double GetDays(DateTime date1, DateTime date2)
        {
            if (IsEndOfMonth(date2))
                date2 = date2.AddDays(1);

            TimeSpan diff = date2 - date1;
            double days = diff.TotalDays;
            return days;
        }

        public static DateTime GetStartOfDay(DateTime date)
        {
            return new DateTime(date.Year, date.Month, date.Day, 0, 0, 0, 0);
        }

        public static DateTime GetEndOfDay(DateTime date)
        {
            return new DateTime(date.Year, date.Month, date.Day, 23, 59, 59, 999);
        }

        #endregion

        #region Periods

        public static DateTime AddYears(DateTime date, Int32 years)
        {
            //return date.AddYears(years);
            return RetainMonthEnd(date, date.AddYears(years));
        }

        public static DateTime AddHalfYears(DateTime date, Int32 halfYears)
        {
            //return date.AddMonths(6 * halfYears);
            return RetainMonthEnd(date, date.AddMonths(6 * halfYears));
        }

        public static DateTime AddQuarters(DateTime date, Int32 quarters)
        {
            //return (date.AddMonths(3 * quarters));
            return RetainMonthEnd(date, date.AddMonths(3 * quarters));
        }

        public static DateTime AddMonths(DateTime date, Int32 months)
        {
            //return date.AddMonths(months);
            return RetainMonthEnd(date, date.AddMonths(months));
        }

        public static DateTime AddDays(DateTime date, Int32 days)
        {
            return date.AddDays(days);
        }

        /// <summary>
        /// This method snaps the new date to the end of the month, if the original date was at the end of a month.
        /// </summary>
        /// <param name="origDate"></param>
        /// <param name="newDate"></param>
        /// <returns></returns>
        private static DateTime RetainMonthEnd(DateTime origDate, DateTime newDate)
        {
            // Check for end of month in original day
            if (origDate.Day != DateTime.DaysInMonth(origDate.Year, origDate.Month))
            {
                return newDate;
            }
            else
            {
                // Retain end-of-month
                return new DateTime(newDate.Year, newDate.Month, DateTime.DaysInMonth(newDate.Year, newDate.Month),
                                    newDate.Hour, newDate.Minute, newDate.Second);
            }
        }

        #endregion

        #region Date/Time Comparison

        public static DateTime TruncateToNearestSecond(DateTime time)
        {
            return new DateTime(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second);
        }

        #endregion Date/Time Comparison

        #region Conversion

        /// <summary>
        /// Converts a date to Universal Time format.
        /// Use this to write dates to Xml, to avoid localisation issues with different cultures.
        /// E.g. 31/12/2008 becomes "2008-12-31 00:00:00Z"
        /// </summary>
        /// <param name="date"></param>
        /// <returns></returns>
        public static string ToString(DateTime date)
        {
            string utc = date.ToString("u", CultureInfo.InvariantCulture);
            return utc;
        }

        /// <summary>
        /// Converts a non-UTC string representation of a Date, to a UTC version
        /// </summary>
        /// <param name="dateString"></param>
        /// <returns></returns>
        public static string ToString(string dateString)
        {
            DateTime date = DateTime.Parse(dateString, CultureInfo.InvariantCulture).ToUniversalTime();
            string utc = date.ToString("u", CultureInfo.InvariantCulture);
            return utc;
        }

        /// <summary>
        /// Converts a non-UTC string or Date representation of a Date, to a UTC version
        /// </summary>
        /// <param name="dateObject"></param>
        /// <returns></returns>
        public static string ToString(object dateObject)
        {
            string dateString = dateObject.ToString();
            DateTime date = DateTime.Parse(dateString, CultureInfo.InvariantCulture).ToUniversalTime();
            string utc = date.ToString("u", CultureInfo.InvariantCulture);
            return utc;
        }

        #endregion

        #region Date Time Checking

        /// <summary>
        /// Determine if the current date and time is a default.
        /// </summary>
        /// <param name="currentDate">The current date time to check.</param>
        /// <returns>If the date time is a default one.</returns>
        public static bool IsDateTimeDefaultDate(DateTime? currentDate)
        {
            if (!currentDate.HasValue || currentDate == DateTime.MinValue)
            {
                return true;
            }
            return false;
        }

        /// <summary>
        /// Determine if the current string date 
        /// repersentation can be converted to a date format.
        /// </summary>
        /// <param name="date">The specified date.</param>
        /// <returns>If the string repersentation of the date value is valid.</returns>
        public static bool IsValidDateType(string date)
        {
            DateTime currentDate;
            if (DateTime.TryParse(date, out currentDate))
            {
                return true;
            }
            return false;
        }

        #endregion

        #region Localisation Culture

        public static string GetShortDateFormat()
        {
            return CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
        }

        #endregion

        public static DateTime Parse(string dateString)
        {
            DateTime result;

            if (!DateTime.TryParseExact(dateString, "u", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal & DateTimeStyles.AssumeUniversal, out result))
            {
                string utcDateString = ToString(dateString);

                if (!DateTime.TryParseExact(utcDateString, "u", CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out result))
                {
                    return DateTime.MinValue;
                }
            }            

            return result;
        }
    }
}